Skip to content

feat(headless): add headless FileUpload primitive#9067

Merged
alexcarpenter merged 2 commits into
mainfrom
headless-file-upload-component
Jul 2, 2026
Merged

feat(headless): add headless FileUpload primitive#9067
alexcarpenter merged 2 commits into
mainfrom
headless-file-upload-component

Conversation

@alexcarpenter

@alexcarpenter alexcarpenter commented Jul 1, 2026

Copy link
Copy Markdown
Member

Description

Adds a new headless, unstyled FileUpload compound primitive to @clerk/headless (following the existing Accordion/Collapsible pattern), composed of Root, Trigger, Dropzone, Item, ItemPreview, and ItemDelete parts plus a useFileUpload hook, supporting file-picker and drag-and-drop selection, single/multiple modes, accept and maxSize filtering with an onReject callback, and object-URL image previews. It ships zero styles (all state is exposed via data-cl-* attributes) and keeps the hidden file input out of the a11y tree so the Trigger button is the accessible control. Also adds a swingset docs page (Primitives layer) and wires the new subpath export. To test: pnpm --filter @clerk/headless test (24 FileUpload cases pass, including accessibility) and view the page via pnpm dev:swingset at /components/file-upload.

Docs: https://swingset-git-headless-file-upload-component.clerkstage.dev/primitives/file-upload

Summary by CodeRabbit

Summary of updates

  • New Features

    • Introduced a new headless FileUpload primitive (Root/Trigger/Dropzone/Item/Preview/Delete) with controlled and uncontrolled support.
    • Added ./file-upload subpath export so the primitive is available for import.
    • Implemented file acceptance filtering and image-only thumbnails.
  • Bug Fixes

    • Improved single-file overflow handling: extra accepted files now trigger onReject with reason: "overflow" instead of being discarded silently.
    • Clarified onReject reason reporting for accept/size mismatches and ensured accept checks run before size validation.
  • Documentation / Tests

    • Added README + Storybook docs and a comprehensive automated test suite for the primitive.

Adds a headless, unstyled FileUpload primitive (Root, Trigger, Dropzone, Item, ItemPreview, ItemDelete + useFileUpload hook) supporting file-picker and drag-and-drop selection, single/multiple modes, accept and maxSize filtering with an onReject callback, and image previews. Includes a swingset docs page.
@vercel

vercel Bot commented Jul 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jul 1, 2026 8:05pm
swingset Ready Ready Preview, Comment Jul 1, 2026 8:05pm

Request Review

@changeset-bot

changeset-bot Bot commented Jul 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0fbd138

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 7319ef2c-cf63-4d75-bcd0-88855fea711e

📥 Commits

Reviewing files that changed from the base of the PR and between 2b9c2f3 and 0fbd138.

📒 Files selected for processing (5)
  • packages/headless/src/primitives/file-upload/README.md
  • packages/headless/src/primitives/file-upload/file-upload-root.tsx
  • packages/headless/src/primitives/file-upload/file-upload.test.tsx
  • packages/swingset/src/stories/file-upload.mdx
  • packages/swingset/src/stories/file-upload.stories.tsx
✅ Files skipped from review due to trivial changes (3)
  • packages/swingset/src/stories/file-upload.stories.tsx
  • packages/swingset/src/stories/file-upload.mdx
  • packages/headless/src/primitives/file-upload/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/headless/src/primitives/file-upload/file-upload.test.tsx

📝 Walkthrough

Walkthrough

Adds a new headless FileUpload primitive with validation, compound parts, package exports, tests, README/docs, and Storybook/swingset registration.

Changes

FileUpload Primitive

Layer / File(s) Summary
Validation helper and shared context
packages/headless/src/primitives/file-upload/accept.ts, .../file-upload-context.ts
Adds native accept-string matching and the root/item React contexts with guard hooks plus the consumer hook.
Root state and item parts
.../file-upload-root.tsx, .../file-upload-dropzone.tsx, .../file-upload-trigger.tsx, .../file-upload-item.tsx, .../file-upload-item-preview.tsx, .../file-upload-item-delete.tsx
Defines rejection reasons and root selection flow, and adds the dropzone, trigger, item, preview, and delete primitives.
Package exports and build wiring
.../index.ts, .../parts.ts, packages/headless/package.json, packages/headless/vite.config.ts, packages/headless/README.md, .changeset/headless-file-upload.md
Adds barrel exports, the ./file-upload package entry, build wiring, a README table entry, and a changeset.
Primitive README
packages/headless/src/primitives/file-upload/README.md
Documents usage, parts, hook contract, props, rejection semantics, data attributes, and accessibility behavior.
FileUpload test suite
packages/headless/src/primitives/file-upload/file-upload.test.tsx
Covers slots, trigger/input behavior, drag-and-drop, validation, selection modes, previews, deletion, controlled and disabled states, render customization, context guards, and accessibility checks.
Storybook story and swingset registration
packages/swingset/src/stories/file-upload.mdx, .../file-upload.stories.tsx, packages/swingset/src/components/DocsViewer.tsx, packages/swingset/src/lib/registry.ts
Adds the MDX page, the Storybook story, and swingset docs/registry entries for discovery.

Estimated code review effort: 4 (Complex) | ~60 minutes

Poem

A rabbit hops with files in tow,
Through dropzone grass and button glow.
Preview pops for images bright,
Rejects the rest with proper light,
Hop, hop — uploads feel just right. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new headless FileUpload primitive.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@9067

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@9067

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@9067

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@9067

@clerk/electron

npm i https://pkg.pr.new/@clerk/electron@9067

@clerk/electron-passkeys

npm i https://pkg.pr.new/@clerk/electron-passkeys@9067

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@9067

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@9067

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@9067

@clerk/express

npm i https://pkg.pr.new/@clerk/express@9067

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@9067

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@9067

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@9067

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@9067

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@9067

@clerk/react

npm i https://pkg.pr.new/@clerk/react@9067

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@9067

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@9067

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@9067

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@9067

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@9067

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@9067

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@9067

commit: 0fbd138

@alexcarpenter alexcarpenter changed the title feat(headless): add FileUpload compound primitive feat(headless): add headless FileUpload primitive Jul 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/headless/src/primitives/file-upload/file-upload-root.tsx (1)

45-55: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding a clipPath fallback to the visually-hidden style.

The clip: rect(0,0,0,0) property is legacy CSS; pairing it with clipPath: 'inset(50%)' is the more future-proof visually-hidden pattern.

♻️ Suggested addition
 const visuallyHiddenInputStyle: CSSProperties = {
   position: 'absolute',
   width: 1,
   height: 1,
   padding: 0,
   margin: -1,
   overflow: 'hidden',
   clip: 'rect(0, 0, 0, 0)',
+  clipPath: 'inset(50%)',
   whiteSpace: 'nowrap',
   border: 0,
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/headless/src/primitives/file-upload/file-upload-root.tsx` around
lines 45 - 55, The visually hidden input style in visuallyHiddenInputStyle
should use the more future-proof pattern by adding a clipPath fallback alongside
the existing legacy clip rule. Update the style object used by the file-upload
root so the hidden input remains accessible across browsers, keeping the current
absolute positioning and sizing while including clipPath: 'inset(50%)' in the
visuallyHiddenInputStyle definition.
packages/headless/src/primitives/file-upload/file-upload.test.tsx (1)

245-257: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider testing object-URL revocation.

The PR summary highlights FileUploadItemPreview generating/revoking object URLs, but no test asserts URL.revokeObjectURL is called when an item is removed or the preview unmounts. Adding a spy-based assertion would guard against blob-URL leaks regressing silently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/headless/src/primitives/file-upload/file-upload.test.tsx` around
lines 245 - 257, Add a test in the file-upload items suite to verify object-URL
cleanup in FileUploadItemPreview: spy on URL.revokeObjectURL, render a file item
through Harness, then remove/unmount it and assert revokeObjectURL is called
with the created blob URL. Use the existing FileUploadItemPreview/FileUploadItem
item rendering flow so the test covers the same lifecycle that generates the
preview URL.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/headless/src/primitives/file-upload/file-upload-root.tsx`:
- Around line 78-102: The addFiles callback in file-upload-root is dropping
overflow files in single-file mode without reporting them, so only the first
accepted file is kept and the rest vanish silently. Update addFiles to detect
when multiple is false and more than one file is accepted, then keep the first
file but also surface the remaining accepted files through the rejection path
with an explicit overflow-style rejection so consumers get notified via
onReject. Use the existing addFiles, onReject, accepted, and rejected flow to
preserve current accept/maxSize handling while ensuring every discarded file is
reported.

In `@packages/swingset/src/stories/file-upload.stories.tsx`:
- Around line 28-35: The selected-file list in FileUpload.Stories uses file.name
as the React key, which can collide for duplicate filenames. Update the map in
the file-upload story to use a stable unique identifier per file, either by
deriving a composite key from more file identity or by assigning an id when
files are added, so each FileUpload.Item remains uniquely keyed.

---

Nitpick comments:
In `@packages/headless/src/primitives/file-upload/file-upload-root.tsx`:
- Around line 45-55: The visually hidden input style in visuallyHiddenInputStyle
should use the more future-proof pattern by adding a clipPath fallback alongside
the existing legacy clip rule. Update the style object used by the file-upload
root so the hidden input remains accessible across browsers, keeping the current
absolute positioning and sizing while including clipPath: 'inset(50%)' in the
visuallyHiddenInputStyle definition.

In `@packages/headless/src/primitives/file-upload/file-upload.test.tsx`:
- Around line 245-257: Add a test in the file-upload items suite to verify
object-URL cleanup in FileUploadItemPreview: spy on URL.revokeObjectURL, render
a file item through Harness, then remove/unmount it and assert revokeObjectURL
is called with the created blob URL. Use the existing
FileUploadItemPreview/FileUploadItem item rendering flow so the test covers the
same lifecycle that generates the preview URL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: d9d1fb65-8b21-405e-a414-fcfdb67bc724

📥 Commits

Reviewing files that changed from the base of the PR and between c01b937 and 2b9c2f3.

📒 Files selected for processing (20)
  • .changeset/headless-file-upload.md
  • packages/headless/README.md
  • packages/headless/package.json
  • packages/headless/src/primitives/file-upload/README.md
  • packages/headless/src/primitives/file-upload/accept.ts
  • packages/headless/src/primitives/file-upload/file-upload-context.ts
  • packages/headless/src/primitives/file-upload/file-upload-dropzone.tsx
  • packages/headless/src/primitives/file-upload/file-upload-item-delete.tsx
  • packages/headless/src/primitives/file-upload/file-upload-item-preview.tsx
  • packages/headless/src/primitives/file-upload/file-upload-item.tsx
  • packages/headless/src/primitives/file-upload/file-upload-root.tsx
  • packages/headless/src/primitives/file-upload/file-upload-trigger.tsx
  • packages/headless/src/primitives/file-upload/file-upload.test.tsx
  • packages/headless/src/primitives/file-upload/index.ts
  • packages/headless/src/primitives/file-upload/parts.ts
  • packages/headless/vite.config.ts
  • packages/swingset/src/components/DocsViewer.tsx
  • packages/swingset/src/lib/registry.ts
  • packages/swingset/src/stories/file-upload.mdx
  • packages/swingset/src/stories/file-upload.stories.tsx

Comment thread packages/headless/src/primitives/file-upload/file-upload-root.tsx
Comment thread packages/swingset/src/stories/file-upload.stories.tsx
In single-file mode, extra files from a multi-file drop were kept-first and silently discarded. Report them through onReject with a new 'overflow' rejection reason. Also key the swingset preview list by a composite file identity to avoid duplicate-filename collisions.
@alexcarpenter alexcarpenter merged commit 2f73bdd into main Jul 2, 2026
51 checks passed
@alexcarpenter alexcarpenter deleted the headless-file-upload-component branch July 2, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants